<template>
	<div id="app">
		<div>
			<!-- verify -->
			<input type="file" :disabled="status !== Status.wait" @change="handleFileChange" />
			<el-button @click="handleUpload" :disabled="uploadDisabled">上传</el-button>
			<el-button @click="handleResume" v-if="status === Status.pause">恢复</el-button>
			<el-button v-else :disabled="status !== Status.uploading || !container.hash" @click="handlePause">暂停
			</el-button>
			<el-button @click="handleDelete">删除</el-button>
		</div>
		<div>
			<div>
				<div>计算块哈希值</div>
				<el-progress :percentage="hashPercentage"></el-progress>
			</div>
			<div>
				<div>百分比</div>
				<el-progress :percentage="fakeUploadPercentage"></el-progress>
			</div>
		</div>
		<el-table :data="data">
			<el-table-column prop="hash" label="chunk hash" align="center"></el-table-column>
			<el-table-column label="大小(KB)" align="center" width="120">
				<template v-slot="{ row }"> {{ row.size | transformByte }} </template>
			</el-table-column>
			<el-table-column label="百分比" align="center">
				<template v-slot="{ row }"> <el-progress :percentage="row.percentage" color="#909399"></el-progress> </template>
			</el-table-column>
		</el-table>
	</div>
</template>
<script>
	// 切片大小
	const SIZE = 10 * 1024 * 1024;
	const Status = {
		wait: "wait",
		pause: "pause",
		uploading: "uploading"
	};
	export default {
		name: "app",
		filters: {
			transformByte(val) {
				return Number((val / 1024).toFixed(0));
			}
		},
		data: () => ({
			Status,
			container: {
				file: null,
				hash: "",
				worker: null
			},
			hashPercentage: 0,
			data: [],
			requestList: [],
			status: Status.wait,
			// 当暂停时会取消 xhr 导致进度条后退
			// 为了避免这种情况，需要定义一个假的进度条
			fakeUploadPercentage: 0
		}),
		computed: {
			uploadDisabled() {
				return (
					!this.container.file || [Status.pause, Status.uploading].includes(this.status)
				);
			},
			uploadPercentage() {
				if (!this.container.file || !this.data.length) return 0;
				const loaded = this.data
					.map(item => item.size * item.percentage)
					.reduce((acc, cur) => acc + cur);
				return parseInt((loaded / this.container.file.size).toFixed(2));
			}
		},
		watch: {
			uploadPercentage(now) {
				if (now > this.fakeUploadPercentage) {
					this.fakeUploadPercentage = now;
				}
			}
		},
		methods: {
			async handleDelete() {
				const { data } = await this.request({
					url: "http://localhost:3000/delete"
				});
				if (JSON.parse(data).code === 0) {
					this.$message.success("文件删除成功");
				}
			},
			// 暂停事件
			handlePause() {
				this.status = Status.pause;
				this.resetData();
			},
			resetData() {
				this.requestList.forEach(xhr => xhr?.abort());
				this.requestList = [];
				if (this.container.worker) {
					this.container.worker.onmessage = null;
				}
			},
			// 恢复事件
			async handleResume() {
				this.status = Status.uploading;
				const { uploadedList } = await this.verifyUpload(
					this.container.file.name,
					this.container.hash
				);
				await this.uploadChunks(uploadedList);
			},
			request({ url, method = "post", data, headers = {}, onProgress = e => e, requestList }) {
				return new Promise(resolve => {
					const xhr = new XMLHttpRequest();
					xhr.upload.onprogress = onProgress;
					xhr.open(method, url);
					Object.keys(headers).forEach(key =>
						xhr.setRequestHeader(key, headers[key])
					);
					xhr.send(data);
					console.log(this.requestList, 123123)
					xhr.onload = e => {
						// 将请求成功的 xhr 从列表中删除
						if (requestList) {
							const xhrIndex = requestList.findIndex(item => item === xhr);
							requestList.splice(xhrIndex, 1);
						}
						resolve({
							data: e.target.response
						});
					};
					// 暴露当前 xhr 给外部
					requestList?.push(xhr);
				});
			},
			// 生成文件切片
			createFileChunk(file, size = SIZE) {
				const fileChunkList = [];
				let cur = 0;
				while (cur < file.size) {
					fileChunkList.push({
						file: file.slice(cur, cur + size)
					});
					cur += size;
				}
				return fileChunkList;
			},
			// 生成文件 hash（web-worker），更新进度条
			calculateHash(fileChunkList) {
				return new Promise(resolve => {
					this.container.worker = new Worker("/hash.js");
					this.container.worker.postMessage({
						fileChunkList
					});
					this.container.worker.onmessage = e => {
						const { percentage, hash } = e.data;
						// 文件上传进度
						this.hashPercentage = percentage;
						if (hash) {
							resolve(hash);
						}
					};
				});
			},
			// 上传事件
			handleFileChange(e) {
				const [file] = e.target.files;
				if (!file) return;
				this.resetData();
				// Object.assign(this.$data, this.$options.data());
				this.container.file = file;
			},
			async handleUpload() {
				if (!this.container.file) return;
				this.status = Status.uploading;
				// 切片列表【】，个数
				const fileChunkList = this.createFileChunk(this.container.file);
				this.container.hash = await this.calculateHash(fileChunkList);
				const { shouldUpload, uploadedList } = await this.verifyUpload(
					this.container.file.name,
					this.container.hash
				);
				if (!shouldUpload) {
					this.$message.success("文件上传成功");
					this.status = Status.wait;
					return;
				}

				this.data = fileChunkList.map(({ file }, index) => ({
					fileHash: this.container.hash,
					index,
					hash: this.container.hash + "-" + index,
					chunk: file,
					size: file.size,
					percentage: uploadedList.includes(index) ? 100 : 0
				}));
				await this.uploadChunks(uploadedList);
			},
			// 上传切片，同时过滤已上传的切片
			async uploadChunks(uploadedList = []) {
				const requestList = this.data
					.filter(({ hash }) => !uploadedList.includes(hash))
					.map(({ chunk, hash, index }) => {
						const formData = new FormData();
						formData.append("chunk", chunk);
						formData.append("hash", hash);
						formData.append("filename", this.container.file.name);
						formData.append("fileHash", this.container.hash);
						return { formData, index };
					})
					.map(({ formData, index }) =>
						this.request({
							url: "http://localhost:3000",
							data: formData,
							onProgress: this.createProgressHandler(this.data[index]),
							requestList: this.requestList
						})
					);
				await Promise.all(requestList);
				// 之前上传的切片数量 + 本次上传的切片数量 = 所有切片数量时合并切片
				if (uploadedList.length + requestList.length === this.data.length) {
					await this.mergeRequest();
				}
			},
			// 通知服务端合并切片
			async mergeRequest() {
				await this.request({
					url: "http://localhost:3000/merge",
					headers: {
						"content-type": "application/json"
					},
					data: JSON.stringify({
						size: SIZE,
						fileHash: this.container.hash,
						filename: this.container.file.name
					})
				});
				this.$message.success("上传成功");
				this.status = Status.wait;
			},
			// 根据 hash 验证文件是否曾经已经被上传过 chunk hash
			// 没有才进行上传
			async verifyUpload(filename, fileHash) {
				const { data } = await this.request({
					url: "http://localhost:3000/verify",
					headers: { "content-type": "application/json" },
					data: JSON.stringify({ filename, fileHash })
				});
				return JSON.parse(data);
			},
			// 用闭包保存每个 chunk 的进度数据
			createProgressHandler(item) {
				return e => {
					item.percentage = parseInt(String((e.loaded / e.total) * 100));
				};
			}
		}
	};
</script>
